package group_1.MultiSnake;

import java.util.ArrayList;

import org.anddev.andengine.engine.FixedStepEngine;
import org.anddev.andengine.engine.options.EngineOptions;
import org.anddev.andengine.entity.scene.background.ColorBackground;
import org.anddev.andengine.entity.sprite.Sprite;
import org.anddev.andengine.opengl.texture.region.TextureRegion;

import android.app.ProgressDialog;
import android.content.Context;
import android.os.Vibrator;
import client_server.IO.HandleServerData;
import client_server.IO.NetworkIO;

/**
 * The game engine running the game loop and handling gamelogic.
 * 
 */
public class SnakeGameEngine extends FixedStepEngine implements SnakeConstants, HandleServerData{
	// Different actions sent/recieved from/to the game server
	private static final String ACTION_TURN = "turn";
	private static final String ACTION_DEAD = "dead";
	private static final String ACTION_START_GAME = "start";
	private static final String ACTION_FIRE = "fire";
	
	private static final double MIN_TURN_DISTANCE = 30; // Used to determine if snake should turn or use item
	
	
	private Context context;
	
	// Containers for all images used during game play
	private TextureRegion bodyTextureRegion, bodyTurnTextureRegion, headTextureRegion, bulletTextureRegion;
	
	private NetworkIO networkIO; 		// Used for all contact to game server

	private ArrayList<Snake> snakes;	// List of all snakes in the game
	private ArrayList<Head> heads;		// List of all the heads of the snakes
	private ArrayList<Item> items;		// List of all items on the gamefield
	
	private boolean gameOver = false;	// Determines if the game is over
	private boolean gameRunning = false;// Determines if the game is running
	private int MY_PLAYER_ID;			// Holds the players id
	
	private ProgressDialog progressDialog;
	
	/**
	 * Constructor - construct a new SnakeGameEngine
	 * 
	 * @param pEngineOptions
	 * @param pStepsPerSecond
	 * @param context
	 */
	public SnakeGameEngine(EngineOptions pEngineOptions, int pStepsPerSecond, Context context) {
		super(pEngineOptions, pStepsPerSecond);
		this.context = context;
		networkIO = new NetworkIO(this);
	}
	
	/**
	 * Creates a new game. Initiate everything needed to play
	 * @param bodyTextureRegion holds image representing a bodypart
	 * @param bodyTurnTextureRegion holds image representing a turn bodypart
	 * @param headTextureRegion holds image representing the head of the snake
	 * @param bulletTextureRegion holds image representing a bullet
	 * @param gameInfo
	 * @param dialog
	 */
	public void newGame(TextureRegion bodyTextureRegion, TextureRegion bodyTurnTextureRegion,
			TextureRegion headTextureRegion, TextureRegion bulletTextureRegion, String gameInfo, ProgressDialog dialog) {
		progressDialog = dialog;
		
		networkIO.joinGame(gameInfo);
		
		this.bodyTextureRegion = bodyTextureRegion;
		this.headTextureRegion = headTextureRegion;
		this.bodyTurnTextureRegion = bodyTurnTextureRegion;
		this.bulletTextureRegion = bulletTextureRegion;
		
		snakes = new ArrayList<Snake>();
		heads = new ArrayList<Head>();
		items = new ArrayList<Item>();
		
	}
	
	/**
	 * Attach all currently existing bodyparts and heads to the scene
	 */
	private void attachInitChildren() {
		for(Snake snake : snakes) {
			for(Sprite bodyPart : snake.getBodyParts())
				this.getScene().attachChild(bodyPart);
			
			this.getScene().attachChild(snake.getHead());
		}
	}
	
	/**
	 * Retrieves all heads from all snakes
	 */
	private void getAllHeads() {
		for(Snake snake : snakes)
			heads.add(snake.getHead());
	}
	
	/**
	 * This is the game loop.
	 * Every loop the game state is updated. Snakes are moved and the players snake is tested
	 * for collision.
	 */
	@Override
	public void onUpdate(final long pNanosecondsElapsed) throws InterruptedException {
		super.onUpdate(pNanosecondsElapsed);
		if(!gameOver && gameRunning) {
			moveSnakes();
			
			// Update item states
			for(Item item : items) {
				item.loopAction();
				this.getScene().detachChild(item);
				this.getScene().attachChild(item);
			}
			
			// Test if bullet hit a bodypart
			testBulletCollisions();
			
			// Test if the players snake was killed
			if(snakes.get(MY_PLAYER_ID).isAlive()) {
				if(testCollision()) {
					gameOver();
				}
			}
		}
	}
	
	/**
	 * Test if a bullet has hit a bodypart
	 */
	private void testBulletCollisions() {
		for(Item item : items) {
			if(item instanceof Bullet) {
				for(Snake snake : snakes) {
					for(BodyPart part : snake.getBodyParts()) {
						if(item.collidesWith(part)){
							items.remove(item);
							this.getScene().detachChild(item);
							snake.hit(part);
							this.getScene().detachChild(part);
							return;
						}
							
					}
				}
			}
		}
	}
	
	/**
	 * Called when the players snake has died.
	 * Send data to server that the snake is dead.
	 */
	private void gameOver() {
		this.getScene().setBackground(new ColorBackground(0, 0, 0));
		
		// Vibrate the mobile phone
		Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
		vibrator.vibrate(150);
		
		Snake snake = snakes.get(MY_PLAYER_ID);
		float x = snake.getLastBodyPart().getFrontX();
		float y = snake.getLastBodyPart().getFrontY();
		String data = ACTION_DEAD + ";" + MY_PLAYER_ID + ";" + x + ";" + y;
		sendDataToServer(data);
		snakes.get(MY_PLAYER_ID).kill();
	}
	
	/**
	 * Handle user screen touch input. 
	 * Calculate if turn snake or use item and send the action to the game server.
	 * 
	 * @param oldX the x-position of first touchevent
	 * @param oldY the y-position of first touchevent
	 * @param newX the x-position of last touchevent
	 * @param newY the y-position of last touchevent
	 */
	public void posInputOperation(float oldX, float oldY, float newX, float newY) {
		if(!gameOver && gameRunning) {
			double distance = Math.hypot(newX-oldX, newY-oldY);
			// If the user swype distance was shorter than a cerain distance the item is used
			if(distance <= MIN_TURN_DISTANCE) {
				Snake snake = snakes.get(MY_PLAYER_ID);
				String data = ACTION_FIRE + ";" + MY_PLAYER_ID + ";" + snake.getFrontX() + ";" + 
				snake.getFrontY();
				sendDataToServer(data);
			}else { // otherwise the snake should turn
				int direction = calcDirection(oldX, oldY, newX, newY);
				
				// Send new direction to server
				Snake snake = snakes.get(MY_PLAYER_ID);
				
				if(snake.validNewDirection(direction)) {
					// No server at the moment. Just set the current pos as the position to turn
					String data = ACTION_TURN + ";" + MY_PLAYER_ID + ";" + snake.getLastBodyPart().getFrontX() + ";" + 
						snake.getLastBodyPart().getFrontY() + ";" + direction;
					sendDataToServer(data);
				}
			}
		}
	}
	
	/**
	 * Use the item currently hold by the snake
	 * 
	 * @param snakeId
	 */
	private void useItem(int snakeId) {
		Snake snake = snakes.get(snakeId);
		Item item = snake.useItem();
		if(item != null) {
			if(item instanceof Bullet) {
				item.setPosition(snake.getFrontX(), snake.getFrontY());
				items.add(item);
				this.getScene().attachChild(item);
			}
		}
	}
	
	/**
	 * Send data to the game server.
	 * 
	 * @param data the data to send to the game server
	 */
	public void sendDataToServer(String data) {
		// No server instead send directly to handleServerData
		// Test server delay
//		try {
//			Thread.sleep(150);
//		}catch(Exception e) {
//			e.printStackTrace();
//		}
		if(snakes.get(MY_PLAYER_ID).isAlive()) {
			networkIO.sendData(data);
//			handleServerData(data);
		}
	}
	
	/**
	 * Called when data arrives from server. Perform different tasks depending
	 * on the data received from server.
	 * @param data String containing all data received from the server.
	 */
	@Override
	public void handleServerData(String data) {
		// Format "action;...
		String[] dataStr = data.split(";");
		
		if(dataStr[0].equals(ACTION_TURN)) {  // Format "action;playerId;atX;atY;direction"
			int snakeId = Integer.parseInt(dataStr[1]);
			float atX = Float.parseFloat(dataStr[2]);
			float atY = Float.parseFloat(dataStr[3]);
			int direction = Integer.parseInt(dataStr[4]);
			turnSnake(snakeId, atX, atY, direction);
		}else if (dataStr[0].equals(ACTION_START_GAME)) {  // Format "action;playerId;nrOfPlayers
			MY_PLAYER_ID = Integer.parseInt(dataStr[1]);
			int nrOfPlayers = Integer.parseInt(dataStr[2]);
			for(int i = 0; i < nrOfPlayers; i++)
				addSnake();
			
			getAllHeads();
			attachInitChildren();
//			progressDialog.cancel();
			gameRunning = true;
		}else if (dataStr[0].equals(ACTION_DEAD)) {   // Format "action;playerId;atX;atY
			int snakeId = Integer.parseInt(dataStr[1]);
			float atX = Float.parseFloat(dataStr[2]);
			float atY = Float.parseFloat(dataStr[3]);
			snakes.get(snakeId).moveToPos(atX, atY);
			snakes.get(snakeId).kill();
		}else if (dataStr[0].equals(ACTION_FIRE)) {   // Format "action;playerId;atX;atY
			int snakeId = Integer.parseInt(dataStr[1]);
			float atX = Float.parseFloat(dataStr[2]);
			float atY = Float.parseFloat(dataStr[3]);
			snakes.get(snakeId).moveToPos(atX, atY);
			useItem(snakeId);
		}
	}
	
	/**
	 * Update all snakes' positions.
	 */
	private void moveSnakes() {
		for(Snake snake : snakes)
			snake.move();
	}
	
	/**
	 * Calculate the new direction by determining which direction the longest distance is between
	 * the old and the new point
	 * @param oldX The x-position of the old point
	 * @param oldY The y-position of the old point
	 * @param newX The x-position of the new point
	 * @param newY The y-position of the new point
	 * @return Returns the ID of the new direction
	 */
	private int calcDirection(float oldX, float oldY, float newX, float newY) {
		float xDiff = newX - oldX;
        float yDiff = newY - oldY;
        
        float[] moveValue = new float[4];
        moveValue[Snake.RIGHT] = xDiff;
        moveValue[Snake.LEFT] = -xDiff;
        moveValue[Snake.DOWN] = yDiff;
        moveValue[Snake.UP] = -yDiff;
        
        int maxId = Snake.RIGHT;
        for(int i = 0; i < moveValue.length; i++) {
        	if(moveValue[i] > moveValue[maxId])
        		maxId = i;
        }
		return maxId;
	}
	
	/**
	 * Turn snake into a new direction.
	 * 
	 * @param snakeId The snake to give a new direction
	 * @param atX The x-position where the snake should get a new direction
	 * @param atY The y-position where the snake should get a new direction
	 * @param direction The new direction
	 */
	private void turnSnake(int snakeId, float atX, float atY, int direction) {
		if(snakes.get(snakeId).turn(atX, atY, direction)) {
			BodyPart last = snakes.get(snakeId).getLastBodyPart();
			BodyPart secondLast = snakes.get(snakeId).getSecondLastBodyPart();
			this.getScene().attachChild(last);
			this.getScene().attachChild(secondLast);
			for(Sprite head : heads){
				this.getScene().detachChild(head);
				this.getScene().attachChild(head);
			}
			if(snakes.get(MY_PLAYER_ID).isAlive() && testCollision())
				gameOver();
		}
	}
	
	/**
	 * Test all snakes for collisions.
	 */
	private boolean testCollision() {
		for(Snake snake : snakes) {
			if(testCollision(snake.getBodyParts()))
				return true;
			
			if(testCollision(heads))
				return true;
		}
		
		Snake snake = snakes.get(MY_PLAYER_ID);
		if(snake.getFrontX() > this.getCamera().getWidth() || snake.getFrontX() < 0 ||
				snake.getFrontY() < 0 || snake.getFrontY() > this.getCamera().getHeight()) {
			return true;
		}
		
		return false;
	}
	
	/**
	 * Test if the players snake has collided with anything
	 * 
	 * @param sprites all sprites to test the collision with
	 * @return true if it has collided. False otherwise
	 */
	private synchronized boolean testCollision(ArrayList<? extends Sprite> sprites) {
		Snake snake = snakes.get(MY_PLAYER_ID);
		Head head = snake.getHead();
		for(Sprite sprite : sprites) {
			if(head.getCollisionSurface().collidesWith(sprite) && sprite != head && sprite != snake.getLastBodyPart())
				return true;
		}
		return false;
	}
	
	/**
	 * Creates a new Snake object and adds it to the list of 
	 * all snakes.
	 * @return Returns the new Snake object.
	 */
	public Snake addSnake() {
		Snake snake = createNextPlayer();
		snakes.add(snake);
		return snake;
	}
	
	/**
	 * Creates the next Snake object. Determines where to place the snake and in which
	 * direction to face it.
	 * @return Returns the new Snake object.
	 */
	private Snake createNextPlayer() {
		Snake snake = null;
		if(snakes.size() == 0) {
			snake = new Snake(PLAYER_1_START_X, PLAYER_1_START_Y, Snake.LEFT, headTextureRegion, bodyTextureRegion, bodyTurnTextureRegion);
		}else if (snakes.size() == 1) {
			snake = new Snake(PLAYER_2_START_X, PLAYER_2_START_Y, Snake.RIGHT, headTextureRegion, bodyTextureRegion, bodyTurnTextureRegion);
		}else if (snakes.size() == 2) {
			snake = new Snake(PLAYER_3_START_X, PLAYER_3_START_Y, Snake.DOWN, headTextureRegion, bodyTextureRegion, bodyTurnTextureRegion);
		}
		else {
			snake = new Snake(250, 100, Snake.RIGHT, headTextureRegion, bodyTextureRegion, bodyTurnTextureRegion);
		}
		snake.setItem(new Bullet(0, 0, BULLET_WIDTH, BULLET_HEIGHT, bulletTextureRegion));
		return snake;
	}
	
	/**
	 * Close all connections and threads used.
	 */
	public void shutDown() {
		networkIO.closeConnections();
	}
}
